package chatclient;

import java.io.*;
import java.net.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import libs.Flag;
import libs.Payload;

/**
 * @author Ciaran Liedeman
 * @author Dirk Terblanche
 * @version 0.4
 *
 */
public class Client {

    private MulticastSocket chatSocket;     // socket to receive server packets and broadcasts
    private boolean connected;              // client connect flag
    private InetAddress group;              // broadcast address
    private InetAddress serverAddress;        // server Address
    private final String inetAddressName = "230.0.0.1"; // broadcast address name
    private ClientThread clientThread;      // client packet listner thread
    private String nickname;
    private static int BYTE_ARRAY_LENGTH = 5000;

    // Convert object to bytes using stream
    private ObjectOutputStream outputStream;
    // read bytes from byte stream and insert into packet
    private ByteArrayOutputStream byteStream;

    /** Default Constructor */
    Client() {
        chatSocket = null;
        connected = false;
        nickname = "";

        try {
            group = InetAddress.getByName(inetAddressName);
        } catch (UnknownHostException ex) {
            System.err.println("Error: Unknown Host");
            System.exit(1); // TODO does not have to be fata error?
        }
    }

    /** Connect to server and login */
    public void connect(int port, String host, String nickname) {
        if (isConnected()) {
            System.out.println("Already Connected");
            return;
        }

        this.nickname = nickname;

        try {
            serverAddress = InetAddress.getByName(host);
        } catch (UnknownHostException ex) {
            System.err.println("Error Unknown Host");
            System.exit(1); // TODO does not have to be fatal
        }

        try {
            chatSocket = new MulticastSocket(port);
            chatSocket.joinGroup(group);
        } catch (IOException ex) {
            System.err.println("IO Error");
            return;
        }

        System.out.println("Connected. Socket opened on port: " + port);

        clientThread = new ClientThread();
        clientThread.start();

        //TODO server connect packet

        connected = true;
    }

    /** Disconnect from server */
    public void disconnect() {
        if (!isConnected()) {
            return;
        }    // nothing to be done

        //TODO server dc packet

        clientThread.interrupt();

        try {
            chatSocket.leaveGroup(group);
        } catch (IOException ex) {
            System.err.println("Leave Group Error IO Exception");
            System.exit(1);
        }

        /*try {
        outputStream.close();
        } catch (IOException ex) {
        Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }*/

        chatSocket.close();
        connected = false;
        nickname = "";
        System.out.println("Disconnected");
    }

    /**
     * Send message to server
     * @param message String text to be sent to server
     */
    public void sendMessage(String message) {
        /* Only send if connected to server */
        if (!isConnected()) {
            System.out.println("Not Connected");
            return;
        }

        /* Create Message payload and send */
        sendPayload(new Payload<String>(Flag.CLIENT_MESSAGE, message, nickname, "Server"));
    }

    /**
     * Send message to client that must be whisphered to a specific client
     * @param message - text to be whisphered
     * @param client - client to receive whispher
     */
    public void whispherMessage(String message, String client) {
        //TODO: implement whispher
    }

    /** Return true if client connected to a server */
    public boolean isConnected() {
        return connected;
    }

    /**
     * Send Packet with object encapsulated data to client(s)
     * @param payload
     */
    private void sendPayload(Payload<String> payload) {

        byteStream = new ByteArrayOutputStream(5000);

        try {
            outputStream = new ObjectOutputStream(new BufferedOutputStream(byteStream));
            outputStream.flush();
            outputStream.writeObject(payload);
            outputStream.flush();
        } catch (IOException ex) {
            System.err.println("Output Stream IO Error");
            System.exit(1);
        }

        byte[] buf = byteStream.toByteArray();
        //InetAddress address = null;

        /*try {
            address = InetAddress.getLocalHost(); // TODO hardcoded
        } catch (UnknownHostException ex) {
            System.err.println("Error Unknown Host");
            System.exit(1);
        }*/

        DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddress, 3000);

        try {
            chatSocket.send(packet);
        } catch (IOException ex) {
            System.err.println("IO Error");
            return;
        }
    }

    /**
     * ClientThread that is always listening for incoming packets
     * When a packet is received its header is parsed and then based
     * on the header the client is sent a command
     */
    private class ClientThread extends Thread {

        private volatile boolean listening;
        private DatagramPacket packet;
        private ByteArrayInputStream byteStream;
        private ObjectInputStream inputStream;
        private Payload<String> payload;

        /** Default Constructor */
        ClientThread() {
            listening = true;
        // TODO cleanup code
        }

        @Override
        public void run() {
            InetAddress clientAddress;

            while (listening) {
                try {
                    /* Create packet for receiving incoming packets */
                    byte[] buf = new byte[BYTE_ARRAY_LENGTH];
                    byteStream = new ByteArrayInputStream(buf);
                    packet = new DatagramPacket(buf, buf.length);

                    chatSocket.receive(packet);
                    Thread.sleep(500);  // save some cpu cycles

                    try {
                        inputStream = new ObjectInputStream(new BufferedInputStream(byteStream));
                    } catch (IOException ex) {
                        System.err.println("Input Stream Error");
                        System.exit(1);
                    }

                    clientAddress = packet.getAddress();
                    payload = receivePayload(packet);
                    processPayload(payload, clientAddress);

                } catch (IOException ex) {
                    if (ClientThread.interrupted()) {
                        // swallow exception, thread was interuppted
                        return;
                    } else {
                        System.err.println("IO Exception Client Thread");
                        System.exit(1);
                    }
                } catch (InterruptedException ex) {
                    // Thread interuppted
                    return;
                }
            }
        }

        private void processPayload(Payload<String> payload, InetAddress clientAddress) {
            switch(payload.getFlag()){
                case CLIENT_MESSAGE:
                    System.out.println(payload.getSource() + ": " + payload.getData());
                    break;

                case CLIENT_WHISPHER:
                    System.out.println(payload.getSource() + "whisphers: " + payload.getData());
                    break;
            }
        }

        private Payload<String> receivePayload(DatagramPacket packet) {
            try {
                payload = (Payload<String>) inputStream.readObject();
            } catch (IOException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }

            try {
                inputStream.close();
                byteStream.close();
            } catch (IOException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }

            return payload;
        }
    }
}
